Chapter 10

Navigation, Routing, and Deep Linking

Session 10

Learning Objectives

By the end of this chapter, you will be able to:

1

Navigation Models Overview

Flutter provides two navigation approaches, each suited for different use cases.

Imperative API (Navigator)

Simple push/pop stack-based navigation using Navigator.push, Navigator.pop, and named routes via Navigator.pushNamed. Works well for most apps and quick flows.

Declarative API (Router)

More control and integrates with browser URLs and deep linking. Use when building web apps or complex routing logic (URL-driven state).

Choose Navigator for most mobile apps; adopt Router when you need URL synchronization, nested route parsing, or advanced deep-link handling.

2

Basic Imperative Navigation with Navigator

The Navigator API provides straightforward methods for moving between screens.

Navigation Methods

  • Push: Push a new route (screen) onto the navigation stack using Navigator.push(context, MaterialPageRoute(builder: (_) => NewPage())).
  • Pop: Pop the current route with Navigator.pop(context) or by using system back button.
  • Replace: Use Navigator.pushReplacement to replace the current route or Navigator.pushAndRemoveUntil to clear stacks.

Example Patterns

Push:

Navigator.push(
  context,
  MaterialPageRoute(builder: (context) => DetailsPage(item: item)),
);

Pop:

Navigator.pop(context);

Replace:

Navigator.pushReplacement(
  context,
  MaterialPageRoute(builder: (context) => HomePage()),
);
3

Named Routes (Centralized)

Named routes provide a centralized way to manage navigation throughout your app.

Setting Up Named Routes

Define routes in MaterialApp(routes: {...}) or use onGenerateRoute for dynamic routing. Named routes improve consistency and make deep linking easier.

Pattern

MaterialApp(
  initialRoute: '/',
  routes: {
    '/': (context) => HomePage(),
    '/details': (context) => DetailsPage(),
  },
);

Navigate:

Navigator.pushNamed(context, '/details', arguments: {'id': 42});

Handle arguments in target:

final args = ModalRoute.of(context)!.settings.arguments as Map;
final id = args['id'];

Guideline: Use onGenerateRoute for argument validation and to avoid brittle casting at the call site.

4

Passing Data and Receiving Results

Flutter provides flexible ways to pass data between screens and receive results.

Passing Data

Pass data via constructors when pushing:

Navigator.push(context, MaterialPageRoute(builder: (_) => EditPage(item: item)));

Receiving Results

Return results using Navigator.pop(context, result) and await the push call:

final result = await Navigator.push(
  context, 
  MaterialPageRoute(builder: (_) => FormPage())
);
if (result != null) { /* handle result */ }

Use cases: Form submission, selection dialogs, and confirmation screens.

5

Route Lifecycle and State Preservation

Understanding route lifecycle helps you manage state and resources effectively.

State Preservation

  • When pushing new routes, previous route states are preserved by default (widgets remain in the tree but may be inactive).
  • For heavy stateful screens with controllers, ensure proper disposal in dispose() and consider caching strategies if route recreation is expensive.
  • Use PageStorage and PageStorageKey to preserve scroll positions and basic widget state across navigation.

Example: Preserve ListView Scroll Position

ListView.builder(
  key: PageStorageKey('courseList'),
  controller: _controller,
  itemCount: ...
)
6

Nested Navigation and Independent Stacks

Complex apps often require independent navigation stacks for different sections.

Useful Pattern

BottomNavigationBar with independent navigation stacks per tab so users can switch tabs and resume interaction where they left off. Implement with an IndexedStack to keep tab screens alive and a Navigator per tab to manage its own stack.

Pattern Outline

  • Top-level Scaffold with BottomNavigationBar
  • Body: IndexedStack of Navigator widgets, each with its own onGenerateRoute and NavigatorKey

Key Points

  • Use a GlobalKey<NavigatorState> per tab to push/pop within that tab.
  • Manage back button behavior: if active tab's Navigator can pop, pop it; otherwise, exit app or switch to default tab.
7

Drawer and Modal Routes

Drawers and modals provide alternative navigation patterns.

Drawer Navigation

Drawer navigation typically replaces or pushes routes; choose consistent behavior to avoid surprising users.

Modal Routes

Modal routes (showDialog, showModalBottomSheet) overlay content; they return results via Navigator.pop.

Example: showDialog Returns Result

final confirmed = await showDialog(
  context: context, 
  builder: (_) => ConfirmDialog()
);
if (confirmed == true) { /* user confirmed */ }
8

Declarative Routing with Router (Brief Guide)

The Router API provides advanced routing capabilities for complex applications.

When to Use Router

Use Router, RouterDelegate, and RouteInformationParser when building web-first apps or complex URL-driven flows. High-level idea: parse incoming URL into a configuration object, let RouterDelegate build a stack of Pages from that configuration, and update browser history when configuration changes.

When to Pick Router

  • Your app must reflect URL path segments and query parameters.
  • You need deep linking integrated with complex nested navigation or restoration via URLs.

Note: Router has a steeper learning curve but offers full control over URL to UI mapping and back stack.

9

Deep Linking and App Links

Deep links allow external sources to open your app to specific screens.

Platform Integrations

  • Android: Configure intent filters in AndroidManifest.xml (app links for verified domains).
  • iOS: Configure Universal Links with Associated Domains and URL types.
  • Flutter: Use packages like uni_links, receive_sharing_intent, or flutter_branch_io for richer integrations.

Flow

  • App receives incoming URI string via platform channel or package API.
  • Parse the URI into route name and arguments.
  • Navigate to the appropriate screen, considering whether the app was cold-started or already running.

Example Deep Link Handler (Conceptual)

void handleIncomingUri(Uri uri) {
  if (uri.pathSegments.isEmpty) return;
  switch (uri.pathSegments.first) {
    case 'course':
      final id = uri.queryParameters['id'];
      Navigator.pushNamed(context, '/course', arguments: {'id': id});
      break;
  }
}

Guideline: Support both initial link handling (on app start) and link stream (while app running).

10

Handling Edge Cases and Security

Proper error handling and security are essential for robust navigation.

Best Practices

  • Validate all deep-link parameters server-side where necessary; do not trust client-provided IDs for sensitive operations.
  • Gracefully handle missing or invalid arguments by showing fallback screens or friendly error messages.
  • Avoid pushing duplicate routes for the same content; deduplicate by checking existing route stack or using pushReplacement when appropriate.
11

Testing Navigation

Testing navigation ensures your app's routing works correctly.

Widget Testing

  • Write widget tests that pump the widget and use tester.tap and tester.pumpAndSettle() to assert navigation outcomes.
  • For Router-based apps, test RouteInformationParser and RouterDelegate in isolation by providing fake route information and verifying built pages.

Example Widget Test Pattern

Pump the app widget, tap navigation button, await pumpAndSettle, assert new screen text exists using find.text.

12

Best Practices and Organization

Organizing navigation code properly makes maintenance easier.

Best Practices

  • Centralize route names and argument contracts in a single file or enum to avoid string typos.
  • Keep navigation triggers in UI layers and navigation logic in coordinators/services when apps grow.
  • Prefer passing strongly-typed objects in constructors rather than raw maps when possible; define a RouteArguments model to reduce runtime casting errors.
  • Document route contracts (expected arguments and types) for team clarity.

Suggested File Layout

  • lib/routes.dart — route name constants and helper navigation functions
  • lib/navigation/ — navigator keys, coordinators, and tab navigation helpers
  • lib/screens/ — individual screen widgets (each screen owns its own argument parsing)
13

Exercises

Practice what you've learned with these exercises:

1. Simple push/pop

Implement HomePage with a list of items. On tap, push DetailsPage passing the tapped item. On DetailsPage, include a Delete button that pops with a boolean result; on HomePage, remove the item if result is true.

2. Named routes and arguments

Configure named routes and navigate via Navigator.pushNamed, passing a typed CourseArgs object. On the target page, use ModalRoute to extract and display fields.

3. BottomNavigation with independent stacks

Build a 3-tab app (Home, Search, Profile) where each tab maintains its own navigation stack. Implement back button handling so back pops the active tab's stack first.

4. Deep-link simulation

Simulate receiving a deep link URI that navigates to a course detail screen. Handle both cold start (app not running) and warm start (app in background) scenarios using a package like uni_links (conceptually if environment constraints prevent full integration).

5. Router basics (optional advanced)

Implement a simple Router-based app where URL paths / and /profile/:id map to HomePage and ProfilePage respectively. Verify resizing the browser updates the displayed route if applicable.

14

Session Assignment

Complete Exercises 1–3 with well-typed argument models and include unit/widget tests for navigation actions. Provide a short README describing how you organized routes, why you chose Navigator vs Router, and how you handled state restoration for each tab.